/*Copyright (c) 2008 Martin Hinks

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.IO;
using System.Reflection;
using System.Collections.Generic;
using System.Net;
using System.Xml;
using System.Xml.XPath;

namespace Impersonation
{
    internal class Impersonator
    {
        private bool _done = false;
        private string _cookie = string.Empty;
        private string _postData = string.Empty;

        public string PostData
        {
            get { return _postData; }
            set { _postData = value; }
        }

        public string Cookie
        {
            get { return _cookie; }
            set { _cookie = value; }
        }

        public bool Done
        {
            get { return _done; }
            set { _done = value; }
        }

        internal Impersonator()
        {
        }


        internal void Run(string xmlDir, string module, string action, string cookiestring)
        {
            RunXML(xmlDir, module, action, cookiestring);

            Output.SendMessageEnd();
        }

        internal Dictionary<string, string> ParseElement(XmlReader xr, string[] compulsoryattributes, string[] optionalattributes)
        {
            Dictionary<string, string> element = new Dictionary<string, string>();
            bool fatalerror = false;
            int reqlinenum = ((IXmlLineInfo)xr).LineNumber;
            string elementname = xr.Name;

            while (xr.MoveToNextAttribute())
            {
                bool found = false;

                if (compulsoryattributes != null)
                {
                    foreach (string s in compulsoryattributes)
                    {
                        if (xr.Name == s)
                        {
                            found = true;

                            if (xr.Value != string.Empty)
                            {
                                element.Add(xr.Name, xr.Value);
                            }
                            else
                            {
                                //Add the element so we don't flag the same problem twice
                                element.Add(xr.Name, xr.Value);
                                Output.SendMessage("ERROR: \"" + elementname + "\" element at line " + reqlinenum + " had an empty attribute \"" + s + "\".");
                                fatalerror = true;
                            }
                        }
                    }
                }

                if (optionalattributes != null)
                {
                    foreach (string s in optionalattributes)
                    {
                        if (xr.Name == s)
                        {
                            found = true;

                            if (xr.Value != string.Empty)
                            {
                                element.Add(xr.Name, xr.Value);
                            }
                            else
                            {
                                //Add the element so we don't flag the same problem twice
                                element.Add(xr.Name, xr.Value);
                                Output.SendMessage("ERROR: \"" + elementname + "\" element at line " + reqlinenum + " had an empty attribute \"" + s + "\".");
                                fatalerror = true;
                            }
                        }
                    }
                }

                if (!found)
                {
                    Output.SendMessage("ERROR: \"" + elementname + "\" element at line " + reqlinenum + " had an unrecognised attribute \"" + xr.Name + "\".");
                    fatalerror = true;
                }
            }

            //Check we have all required elements
            if (compulsoryattributes != null)
            {
                foreach (string s in compulsoryattributes)
                {
                    if (!element.ContainsKey(s))
                    {
                        fatalerror = true;
                        Output.SendMessage("ERROR: \"" + elementname + "\" element at line " + reqlinenum + " is missing attribute \"" + s + "\".");
                    }
                }
            }

            if (fatalerror)
            {
                //Mark a failure
                element.Add("fatalerror", "fatalerror");
            }

            return element;

        }

        internal Dictionary<int, KeyValuePair<string, Dictionary<string, string>>> ParseSubElements(XPathNavigator xpn)
        {
            return null;
        }

        internal bool RunXML(string xmlDir, string module, string action, string cookiestring)
        {
            //Look for an XML file
            if (File.Exists(xmlDir + "\\" + module + ".xml"))
            {
                XmlReader xr = null;
                bool run = true;
                Dictionary<string, string> storedvariables = new Dictionary<string, string>();
                List<ImpersonatedRequest> requests = new List<ImpersonatedRequest>();
                string output = string.Empty;

                try
                {
                    try
                    {
                        xr = XmlReader.Create(xmlDir + "\\" + module + ".xml", null);
                        Output.SendMessageStart("Loaded module \"" + module + "\" and action \"" + action + "\" from \"" + module + ".xml\".");
                    }
                    catch (Exception)
                    {
                        Output.SendMessageStart("\"" + module + ".xml\" appears to be malformed.");
                        return false;
                    }

                    while (xr.Read())
                    {
                        if (xr.IsStartElement())
                        {
                            switch (xr.Name.ToLowerInvariant())
                            {
                                case "requires":
                                    int requlinenum = ((IXmlLineInfo)xr).LineNumber;
                                    string requelementname = xr.Name;
                                    Dictionary<string, string> RequiresElement = ParseElement(xr, new string[] { "name", "type", "storein" }, null);

                                    if (RequiresElement.ContainsKey("fatalerror"))
                                    {
                                        run = false;
                                    }
                                    else
                                    {
                                        switch (RequiresElement["type"])
                                        {
                                            case "querystring":
                                                if (HttpContext.Current.Request.QueryString[RequiresElement["name"]] == null || HttpContext.Current.Request.QueryString[RequiresElement["name"]] == string.Empty)
                                                {
                                                    Output.SendMessage("ERROR: Module requires the querystring (GET) parameter \"" + RequiresElement["name"] + "\" which was not provided.");
                                                    run = false;
                                                }
                                                else
                                                {
                                                    storedvariables.Add(RequiresElement["storein"], HttpContext.Current.Request.QueryString[RequiresElement["name"]]);
                                                }
                                                break;
                                            default:
                                                run = false;
                                                Output.SendMessage("ERROR: Attribute \"type\" of \"" + requelementname + "\" element at line " + requlinenum + " was an unrecognised type.");
                                                break;
                                        }
                                    }
                                    break;
                                case "request":
                                    int reqlinenum = ((IXmlLineInfo)xr).LineNumber;
                                    string elementname = xr.Name;
                                    XmlReader xrRequestSubTree = xr.ReadSubtree();

                                    Dictionary<string, string> RequestElement = ParseElement(xr, new string[] { "url", "type" }, null);

                                    if (RequestElement.ContainsKey("fatalerror"))
                                    {
                                        run = false;
                                    }
                                    else
                                    {
                                        RequestType rt;

                                        switch (RequestElement["type"].ToLowerInvariant())
                                        {
                                            case "post":
                                                rt = RequestType.POST;
                                                break;
                                            default:
                                                rt = RequestType.GET;
                                                break;
                                        }

                                        ImpersonatedRequest r = new ImpersonatedRequest(rt, RequestElement["url"], storedvariables, this);

                                        //Parse sub elements
                                        while (xrRequestSubTree.Read())
                                        {
                                            if (xrRequestSubTree.IsStartElement())
                                            {
                                                switch (xrRequestSubTree.Name.ToLowerInvariant())
                                                {
                                                    case "setcookie":
                                                        Dictionary<string, string> SetCookieElement = ParseElement(xrRequestSubTree, new string[] { "name", "type" }, null);
                                                        if (SetCookieElement.ContainsKey("fatalerror"))
                                                        {
                                                            run = false;
                                                        }
                                                        else
                                                        {
                                                            switch (SetCookieElement["type"])
                                                            {
                                                                case "querystring":
                                                                    r.CookieType = CookieSetType.QueryString;
                                                                    r.Cookie = SetCookieElement["name"];
                                                                    break;
                                                                case "persist":
                                                                    r.CookieType = CookieSetType.Persist;
                                                                    r.Cookie = SetCookieElement["name"];
                                                                    break;
                                                            }
                                                        }
                                                        break;
                                                    case "storevariable":
                                                        //"type" does nothing at the moment
                                                        int svlinenum = ((IXmlLineInfo)xrRequestSubTree).LineNumber;
                                                        string svelementname = xrRequestSubTree.Name;

                                                        Dictionary<string, string> StoreVariableElement = ParseElement(xrRequestSubTree, new string[] { "name", "pattern", "group", "type" }, null);
                                                        if (StoreVariableElement.ContainsKey("fatalerror"))
                                                        {
                                                            run = false;
                                                        }
                                                        else
                                                        {
                                                            int group = 0;
                                                            if (!int.TryParse(StoreVariableElement["group"], out group))
                                                            {
                                                                run = false;
                                                                Output.SendMessage("ERROR: Attribute \"group\" of \"" + svelementname + "\" element at line " + svlinenum + " must be an integer.");
                                                            }
                                                            else
                                                            {
                                                                RegexMatcher rm = new RegexMatcher(group, StoreVariableElement["pattern"], StoreVariableElement["name"]);
                                                                r.VariableSetters.Add(rm);
                                                            }
                                                        }
                                                        break;
                                                    case "postdata":
                                                        Dictionary<string, string> PostDataElement = ParseElement(xrRequestSubTree, new string[] { "value" }, null);
                                                        if (PostDataElement.ContainsKey("fatalerror"))
                                                        {
                                                            run = false;
                                                        }
                                                        else
                                                        {
                                                            r.PostData = PostDataElement["value"];
                                                        }
                                                        break;
                                                }
                                            }
                                        }

                                        requests.Add(r);
                                    }
                                    break;
                                case "output":
                                    output = xr.ReadInnerXml();
                                    break;
                            }
                        }

                    }
                }
                catch (XmlException)
                {
                    run = false;
                    Output.SendMessage("ERROR: Malformed XML at line " + ((IXmlLineInfo)xr).LineNumber + ".");
                }

                xr.Close();

                if (!run)
                {
                    Output.SendMessage("Execution was halted.");
                    return false;
                }


                foreach (ImpersonatedRequest r in requests)
                {
                    switch (r.CookieType)
                    {
                        case CookieSetType.QueryString:
                            Cookie = HttpContext.Current.Request.QueryString[r.Cookie];
                            break;
                        case CookieSetType.Persist:
                            //Cookie is set to any previous Set-Cookie values
                            //by DoRequest()
                            break;
                    }

                    //Replace 
                    // * postdata 
                    // * Regex Matchers
                    // * URLs
                    //with any stored variables
                    foreach (string s in storedvariables.Keys)
                    {
                        //postdata
                        r.PostData = r.PostData.Replace("[VAR:" + s + "]", storedvariables[s]);

                        //Regex Matchers
                        foreach (RegexMatcher rm in r.VariableSetters)
                        {
                            rm.Pattern = rm.Pattern.Replace("[VAR:" + s + "]", storedvariables[s]);
                        }

                        //URL
                        r.Url = r.Url.Replace("[VAR:" + s + "]", storedvariables[s]);
                    }
                    

                    r.DoRequest();
                }

                foreach (string s in storedvariables.Keys)
                {
                    output = output.Replace("[VAR:" + s + "]", storedvariables[s]);
                }

                Output.SendMessage(output);
                return true;
            }
            else
            {
                Impersonation.Output.SendErrorStart("The requested module was not found.");
                return false;
            }
        }   

    }
}
